Skip to content

Method: static {...}

1: /*
2: * Copyright © 2020 Fachhochschule für die Wirtschaft (FHDW) Hannover
3: *
4: * This file is part of gaming-core.
5: *
6: * Gaming-core is free software: you can redistribute it and/or modify it under
7: * the terms of the GNU General Public License as published by the Free Software
8: * Foundation, either version 3 of the License, or (at your option) any later
9: * version.
10: *
11: * Gaming-core is distributed in the hope that it will be useful, but WITHOUT
12: * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13: * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14: * details.
15: *
16: * You should have received a copy of the GNU General Public License along with
17: * gaming-core. If not, see <http://www.gnu.org/licenses/>.
18: */
19: package de.fhdw.gaming.core.domain;
20:
21: import java.util.ArrayList;
22: import java.util.Collection;
23: import java.util.Collections;
24: import java.util.LinkedHashMap;
25: import java.util.LinkedHashSet;
26: import java.util.List;
27: import java.util.Map;
28: import java.util.Objects;
29: import java.util.Optional;
30: import java.util.Set;
31: import java.util.concurrent.CompletionService;
32: import java.util.concurrent.ExecutionException;
33: import java.util.concurrent.ExecutorCompletionService;
34: import java.util.concurrent.ExecutorService;
35: import java.util.concurrent.Executors;
36: import java.util.concurrent.Future;
37: import java.util.concurrent.TimeUnit;
38: import java.util.stream.Collectors;
39:
40: import de.fhdw.gaming.core.domain.util.ConsumerE;
41:
42: /**
43: * Implements {@link Game}.
44: *
45: * @param <S> The type of game states.
46: * @param <M> The type of game moves.
47: * @param <P> The type of game players.
48: * @param <ST> The type of game strategies.
49: */
50: @SuppressWarnings("PMD.GodClass")
51: public abstract class DefaultGame<P extends Player<P>, S extends State<P, S>, M extends Move<P, S>,
52: ST extends Strategy<P, S, M>>
53: implements Game<P, S, M, ST> {
54:
55: /**
56: * The ID of this game.
57: */
58: private final int id;
59: /**
60: * The game state.
61: */
62: private S state;
63: /**
64: * The players of the game together with their strategies.
65: */
66: private final Map<String, ST> strategies;
67: /**
68: * The maximum computation time per move in seconds.
69: */
70: private final long maxComputationTimePerMove;
71: /**
72: * The move checker.
73: */
74: private final MoveChecker<P, S, M> moveChecker;
75: /**
76: * The registered observers.
77: */
78: private final List<Observer> observers;
79: /**
80: * The executor for submitting tasks for choosing a move.
81: */
82: private final ExecutorService executorService;
83: /**
84: * {@code true} if the game has been started, else {@code false}.
85: */
86: private boolean started;
87:
88: /**
89: * Creates a game. Uses
90: * {@link GameBuilder#DEFAULT_MAX_COMPUTATION_TIME_PER_MOVE} as maximum
91: * computation time per move.
92: *
93: * @param id The ID of this game.
94: * @param initialState The initial state of the game.
95: * @param strategies The players' strategies.
96: * @param moveChecker The move checker.
97: * @param observerFactoryProvider The {@link ObserverFactoryProvider}.
98: * @throws IllegalArgumentException if the player sets do not match.
99: * @throws InterruptedException if creating the game has been interrupted.
100: */
101: public DefaultGame(final int id, final S initialState, final Map<String, ST> strategies,
102: final MoveChecker<P, S, M> moveChecker, final ObserverFactoryProvider observerFactoryProvider)
103: throws IllegalArgumentException, InterruptedException {
104:
105: this(id, initialState, strategies, GameBuilder.DEFAULT_MAX_COMPUTATION_TIME_PER_MOVE, moveChecker,
106: observerFactoryProvider);
107: }
108:
109: /**
110: * Creates a game.
111: *
112: * @param id The ID of this game.
113: * @param initialState The initial state of the game.
114: * @param strategies The players' strategies.
115: * @param maxComputationTimePerMove The maximum computation time per move in
116: * seconds.
117: * @param moveChecker The move checker.
118: * @param observerFactoryProvider The {@link ObserverFactoryProvider}.
119: * @throws IllegalArgumentException if the player sets do not match.
120: * @throws InterruptedException if creating the game has been interrupted.
121: */
122: public DefaultGame(final int id, final S initialState, final Map<String, ST> strategies,
123: final long maxComputationTimePerMove, final MoveChecker<P, S, M> moveChecker,
124: final ObserverFactoryProvider observerFactoryProvider)
125: throws IllegalArgumentException, InterruptedException {
126:
127: this.id = id;
128: this.state = Objects.requireNonNull(initialState, "initialState").deepCopy();
129: this.strategies = new LinkedHashMap<>(Objects.requireNonNull(strategies, "players"));
130: this.maxComputationTimePerMove = maxComputationTimePerMove;
131: this.moveChecker = Objects.requireNonNull(moveChecker, "moveChecker");
132: this.executorService = Executors.newCachedThreadPool();
133: this.started = false;
134:
135: if (!strategies.keySet().equals(this.state.getPlayers().keySet())) {
136: throw new IllegalArgumentException(
137: "The set of players defined by the game state must match the set of players "
138: + "associated with strategies.");
139: }
140:
141: this.observers = Collections.synchronizedList(observerFactoryProvider.getObserverFactories().stream()
142: .map(ObserverFactory::createObserver).collect(Collectors.toList()));
143: this.checkAndAdjustPlayerStatesIfNecessary();
144: }
145:
146: /**
147: * Returns a string representing the state of the game.
148: */
149: protected final String gameToString() {
150: return String.format("state=%s, strategies=%s", this.state, this.strategies);
151: }
152:
153: @Override
154: public final int getId() {
155: return this.id;
156: }
157:
158: @Override
159: public final Map<String, P> getPlayers() {
160: return this.state.getPlayers();
161: }
162:
163: @Override
164: public final Map<String, ST> getStrategies() {
165: return this.strategies;
166: }
167:
168: @Override
169: public final S getState() {
170: return this.state.deepCopy();
171: }
172:
173: @Override
174: public final void addObserver(final Observer observer) {
175: this.observers.add(observer);
176: }
177:
178: @Override
179: public final void removeObserver(final Observer observer) {
180: this.observers.remove(observer);
181: }
182:
183: @Override
184: public final void start() throws InterruptedException {
185: for (final ST strategy : this.strategies.values()) {
186: strategy.reset();
187: }
188: this.callObservers((final Observer o) -> o.started(this, this.state.deepCopy()));
189: this.started = true;
190: }
191:
192: /**
193: * Runs all observers.
194: *
195: * @param call Called with the observer as argument.
196: */
197: private void callObservers(final ConsumerE<Observer, InterruptedException> call) throws InterruptedException {
198: final ArrayList<Observer> copyOfObserverList = new ArrayList<>(this.observers);
199: for (final Observer observer : copyOfObserverList) {
200: call.accept(observer);
201: }
202: }
203:
204: @Override
205: public final void makeMove() throws IllegalStateException, InterruptedException {
206: if (!this.isStarted()) {
207: throw new IllegalStateException("Trying to make a move although the game has not been started yet.");
208: }
209: if (this.isFinished()) {
210: throw new IllegalStateException("Trying to make a move although the game is already over.");
211: }
212:
213: final Set<P> nextPlayers = this.state.computeNextPlayers();
214: final S stateCopy = this.state.deepCopy();
215: final LinkedHashSet<
216: P> players = nextPlayers.stream().map((final P player) -> stateCopy.getPlayers().get(player.getName()))
217: .collect(Collectors.toCollection(LinkedHashSet::new));
218: this.callObservers((final Observer o) -> o.nextPlayersComputed(this, stateCopy, players));
219:
220: if (nextPlayers.isEmpty()) {
221: // no active players -> game over
222: this.callObservers((final Observer o) -> o.finished(this, this.state.deepCopy()));
223: return;
224: }
225:
226: final CompletionService<Optional<M>> completionService = new ExecutorCompletionService<>(this.executorService);
227: final Map<Future<Optional<M>>, P> futures = this.submitMoveComputingRequests(nextPlayers, completionService);
228: try {
229: this.applyNextPossibleMove(completionService, futures);
230: } finally {
231: this.state.nextTurn();
232: this.checkAndAdjustPlayerStatesIfNecessary();
233: }
234: }
235:
236: @Override
237: public void abortRequested() {
238: for (final ST strategy : this.strategies.values()) {
239: strategy.abortRequested(this.id);
240: }
241: }
242:
243: @Override
244: public final boolean isStarted() {
245: return this.started;
246: }
247:
248: @Override
249: public final boolean isFinished() {
250: final List<P> playersPlaying = this.getPlayers().values().stream()
251: .filter((final P player) -> player.getState().equals(PlayerState.PLAYING)).collect(Collectors.toList());
252: return playersPlaying.isEmpty();
253: }
254:
255: @Override
256: public final void close() {
257: this.executorService.shutdown();
258: }
259:
260: /**
261: * Places a move choosing task for each active player.
262: *
263: * @param nextPlayers The active players.
264: * @param completionService The completion service.
265: * @return The futures receiving the computation results.
266: */
267: private Map<Future<Optional<M>>, P> submitMoveComputingRequests(final Set<P> nextPlayers,
268: final CompletionService<Optional<M>> completionService) {
269:
270: final Map<Future<Optional<M>>, P> futures = new LinkedHashMap<>(nextPlayers.size());
271: for (final P nextPlayer : nextPlayers) {
272: if (!this.strategies.containsKey(nextPlayer.getName())) {
273: throw new IllegalStateException(String.format("State computed unknown next player %s.", nextPlayer));
274: }
275:
276: final Strategy<P, S, M> strategy = this.strategies.get(nextPlayer.getName());
277: final S stateCopy = this.state.deepCopy();
278: futures.put(
279: completionService.submit(() -> strategy.computeNextMove(this.id,
280: stateCopy.getPlayers().get(nextPlayer.getName()), stateCopy)),
281: nextPlayer);
282: }
283: return futures;
284: }
285:
286: /**
287: * Applies the next available move of the "fastest" player. If some move can be
288: * successfully applied, {@link Observer#legalMoveApplied(Game, State, Player, Move)}
289: * will be called, otherwise
290: * {@link Observer#illegalMoveRejected(Game, State, Player, Optional, String)} will be
291: * invoked for each illegal move returned until a legal move has been applied.
292: * Pending moves or moves computed after a legal move of some player has been
293: * applied are discarded without generating an event.
294: *
295: * @param completionService The completion service.
296: * @param futures The futures receiving the computation results.
297: */
298: private void applyNextPossibleMove(final CompletionService<Optional<M>> completionService,
299: final Map<Future<Optional<M>>, P> futures) throws InterruptedException {
300:
301: Optional<P> playerDoingTheMove = Optional.empty();
302:
303: try {
304: while (!futures.isEmpty()) {
305: playerDoingTheMove = this.tryToApplyNextPossibleMove(completionService, futures);
306: if (playerDoingTheMove.isPresent()) {
307: return;
308: }
309: }
310: } finally {
311: if (!futures.isEmpty()) {
312: assert playerDoingTheMove.isPresent();
313:
314: for (final Map.Entry<Future<Optional<M>>, P> entry : futures.entrySet()) {
315: final Future<Optional<M>> future = entry.getKey();
316: future.cancel(true);
317:
318: final S stateCopy = this.state.deepCopy();
319: final P overtakingPlayer = stateCopy.getPlayers().get(playerDoingTheMove.orElseThrow().getName());
320: final P overtakenPlayer = stateCopy.getPlayers().get(entry.getValue().getName());
321: this.callObservers((final Observer o) -> o.playerOvertaken(this, stateCopy, overtakenPlayer,
322: overtakingPlayer));
323: }
324: }
325: }
326: }
327:
328: /**
329: * Tries to apply the next available move of the "fastest" player. If some move
330: * can be successfully applied,
331: * {@link Observer#legalMoveApplied(Game, State, Player, Move)} will be called,
332: * otherwise {@link Observer#illegalMoveRejected(Game, State, Player, Optional, String)}
333: * will be invoked for such an illegal move.
334: *
335: * @param completionService The completion service.
336: * @param futures The futures receiving the computation results.
337: * @return The player for whom a legal move has been applied successfully (if
338: * any). If no legal move could be applied, an empty Optional is
339: * returned.
340: */
341: private Optional<P> tryToApplyNextPossibleMove(final CompletionService<Optional<M>> completionService,
342: final Map<Future<Optional<M>>, P> futures) throws InterruptedException {
343:
344: final Future<Optional<M>> future = completionService.poll(this.maxComputationTimePerMove, TimeUnit.SECONDS);
345: if (future == null) {
346: // no strategy succeeded in finding a legal move within the configured time
347: // window; choosing random moves
348: for (final Map.Entry<Future<Optional<M>>, P> entry : futures.entrySet()) {
349: final S stateCopy = this.state.deepCopy();
350: final Optional<M> chosenMove = this
351: .chooseRandomMove(stateCopy.getPlayers().get(entry.getValue().getName()), stateCopy);
352: if (chosenMove.isEmpty()) {
353: // No move available; this can happen if a previously chosen random move has won
354: // the game. In this case, we let the following strategies unpunished and do nothing.
355: continue;
356: }
357:
358: this.handleOverdueMove(stateCopy.getPlayers().get(entry.getValue().getName()), chosenMove);
359: try {
360: this.applyMoveIfPossible(stateCopy.getPlayers().get(entry.getValue().getName()), chosenMove.get());
361: } catch (final GameException e) {
362: // the game itself did not succeed in finding a legal random move?!?
363: this.handleIllegalMove(stateCopy.getPlayers().get(entry.getValue().getName()), chosenMove,
364: e.getMessage());
365: }
366: }
367:
368: futures.clear();
369: return Optional.empty();
370: }
371:
372: final P playerDoingTheMove = futures.remove(future);
373: Optional<M> move = Optional.empty();
374: try {
375: move = this.determineNextAvailableMove(future);
376: if (move == null) {
377: // strategy returned null which is not allowed, but we are going to condone it
378: // for now
379: move = Optional.empty();
380: }
381: if (move.isPresent()) {
382: // check if strategy attempts to cheat by returning an unsupported custom move
383: this.checkMove(move.get());
384:
385: this.applyMoveIfPossible(playerDoingTheMove, move.get());
386: return Optional.of(playerDoingTheMove); // some legal move has been found and applied
387: } else {
388: // the player resigned the game
389: playerDoingTheMove.setState(PlayerState.RESIGNED);
390: final S stateCopy = this.state.deepCopy();
391: this.callObservers((final Observer o) -> o.playerResigned(this, stateCopy,
392: stateCopy.getPlayers().get(playerDoingTheMove.getName())));
393: }
394: } catch (final GameException e) {
395: // the strategy did not succeed in finding a legal move (or tried to cheat)
396: this.handleIllegalMove(playerDoingTheMove, move, e.getMessage());
397: }
398:
399: return Optional.empty();
400: }
401:
402: /**
403: * Handles an illegal move.
404: *
405: * @param player The player.
406: * @param move The move if present.
407: * @param reason The reason why the move is illegal.
408: */
409: private void handleIllegalMove(final P player, final Optional<M> move, final String reason)
410: throws InterruptedException {
411: player.setState(PlayerState.LOST);
412: final Optional<Move<?, ?>> moveTried = Optional.ofNullable(move.orElse(null));
413: final S stateCopy = this.state.deepCopy();
414: this.callObservers(
415: (final Observer o) -> o.illegalMoveRejected(this, stateCopy,
416: stateCopy.getPlayers().get(player.getName()), moveTried, reason));
417: }
418:
419: /**
420: * Handles an overdue move.
421: *
422: * @param player The player.
423: * @param chosenMove The move that has been chosen.
424: */
425: private void handleOverdueMove(final P player, final Optional<M> chosenMove) throws InterruptedException {
426: final Optional<Move<?, ?>> moveChosen = Optional.ofNullable(chosenMove.orElse(null));
427: final S stateCopy = this.state.deepCopy();
428: this.callObservers(
429: (final Observer o) -> o.overdueMoveRejected(this, stateCopy,
430: stateCopy.getPlayers().get(player.getName()), moveChosen));
431: }
432:
433: /**
434: * Checks if the move is supported.
435: *
436: * @param move The move to check.
437: * @throws GameException if the move is not supported.
438: */
439: private void checkMove(final M move) throws GameException {
440: if (!this.moveChecker.check(move)) {
441: throw new GameException(String.format("Unsupported move: %s.", move));
442: }
443: }
444:
445: /**
446: * Returns the next available move from a {@link Future}.
447: *
448: * @param future The future.
449: * @return The next available move returned by the strategy.
450: * @throws GameException if the strategy caused an exception to be thrown.
451: */
452: private Optional<M> determineNextAvailableMove(final Future<Optional<M>> future)
453: throws GameException, InterruptedException {
454: try {
455: return future.get();
456: } catch (final ExecutionException e) {
457: final Throwable cause = e.getCause();
458: throw new GameException("The strategy did not succeed in finding a valid move: " + cause.getMessage(), e);
459: }
460: }
461:
462: /**
463: * Applies a move for a given player to the current game state if possible. If
464: * the move can be successfully applied,
465: * {@link Observer#legalMoveApplied(Game, State, Player, Move)} will be called,
466: * otherwise {@link Observer#illegalMoveRejected(Game, State, Player, Optional, String)}
467: * will be invoked for each illegal move returned.
468: *
469: * @param player The current player.
470: * @param move The move to apply.
471: * @throws GameException if the move could not be applied to the current game
472: * state for some reason.
473: */
474: private void applyMoveIfPossible(final P player, final M move) throws GameException, InterruptedException {
475: final S newState = this.state.deepCopy();
476: move.applyTo(newState, newState.getPlayers().get(player.getName()));
477: this.state = newState;
478:
479: final S stateCopy = this.state.deepCopy();
480: this.callObservers((final Observer o) -> o.legalMoveApplied(this, stateCopy,
481: stateCopy.getPlayers().get(player.getName()), move));
482: }
483:
484: /**
485: * Checks and adjusts the states of the players if necessary.
486: */
487: private void checkAndAdjustPlayerStatesIfNecessary() throws InterruptedException {
488: final Collection<P> players = this.getPlayers().values();
489: final List<P> playersPlaying = players.stream()
490: .filter((final P player) -> player.getState().equals(PlayerState.PLAYING)).collect(Collectors.toList());
491: final List<P> playersWon = players.stream()
492: .filter((final P player) -> player.getState().equals(PlayerState.WON)).collect(Collectors.toList());
493:
494: final boolean gameOver;
495: if (playersPlaying.isEmpty()) {
496: // all players have stopped playing
497: gameOver = true;
498: } else if (!playersWon.isEmpty()) {
499: // at least one player has won the game -- no time for losers (Queen)
500: playersPlaying.forEach((final P player) -> player.setState(PlayerState.LOST));
501: gameOver = true;
502: } else if (playersPlaying.size() == 1) {
503: // one player remains -- the winner takes them all (ABBA)
504: playersPlaying.get(0).setState(PlayerState.WON);
505: gameOver = true;
506: } else {
507: // there are at least two players participating at the game
508: gameOver = false;
509: }
510:
511: if (gameOver) {
512: this.callObservers((final Observer o) -> o.finished(this, this.state.deepCopy()));
513: }
514: }
515: }